Skip to content

S05-03 JS高级-对象增强、原型、继承、手写

[TOC]

对象增强▸

Object.defineProperty

对属性操作的控制

Object.defineProperty(obj, prop, descriptor):在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • obj:``,要定义属性的对象
  • prop:``,要定义或修改的属性名或 Symbol
  • descriptor:``,要定义或修改的属性描述符
  • 返回值
  • :``,被传递给函数的对象

在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:

  • 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过 delete 删除的?这个属性是否在 for-in 遍历的时候被遍历出来呢?

image-20230620105158798

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符

  • 通过属性描述符可以精准的添加或修改对象的属性

  • 属性描述符需要使用 Object.defineProperty() 来对属性进行添加或者修改;

属性描述符的类型有两种:

  • 数据属性(Data Properties)描述符(Descriptor);

  • 存取属性(Accessor 访问器 Properties)描述符(Descriptor);

image-20230620105234082

数据属性描述符

数据数据描述符有如下四个特性:

Configurable

[[Configurable]]boolean,表示属性是否可以通过 delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]默认为 true

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false

用法:

▸ 不可删除

image-20230708164320249


▸ 不可重新配置

image-20230708164510871


▸ 通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false

image-20230708164812460

Enumerable

[[Enumerable]]:表示属性是否可以通过 for-in 或者 Object.keys()遍历该属性

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为 true;

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为 false;

image-20230708164956052

Writable

[[Writable]]:表示是否可以修改属性的值;

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为 true;

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为 false;

image-20230708165227192

value

[[value]]:属性的 value 值,读取属性时会返回该值,修改属性时,会对其进行修改;

  • 默认情况下这个值是 undefined;

image-20230708165129600

测试代码

image-20230620105258203

image-20230620105304606

image-20230620105312373

存取属性描述符

存取属性描述符

存取属性描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过 delete 删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 和数据属性描述符是一致的;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为 true;

    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false;

  • [[Enumerable]]:表示属性是否可以通过 for-in 或者 Object.keys()返回该属性;

    • 和数据属性描述符是一致的;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为 true;

    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为 false;

  • [[get]]:获取属性时会执行的函数。默认为 undefined

  • [[set]]:设置属性时会执行的函数。默认为 undefined

存储属性描述符测试代码

image-20230620105326652

Object.defineProperties

Object.defineProperties()方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象。

image-20230620105411913

对象其他方法

获取对象的属性描述符:

  • getOwnPropertyDescriptor

  • getOwnPropertyDescriptors

禁止对象扩展新属性:

  • preventExtensions:给一个对象添加新的属性会失败(在严格模式下会报错);

密封对象,不允许配置和删除属性:

  • seal

    • 实际是调用 preventExtensions

    • 并且将现有属性的 configurable:false

冻结对象,不允许修改现有属性:

  • freeze

    • 实际上是调用 seal

    • 并且将现有属性的 writable: false

示例:

1、getOwnPropertyDescriptor

image-20230708170647493

2、getOwnPropertyDescriptors

image-20230708170757436

3、preventExtensions

image-20230708170934112

4、seal:preventExtensions + configurable: false

image-20230708171106375

5、freeze:seal + writable: false

image-20230708171241763

原型

对象和函数的原型

对象的原型

JavaScript 当中每个对象都有一个特殊的内置属性*[[Prototype]]*,这个特殊的对象可以指向另外一个对象。

那么这个对象有什么作用呢?

  • 当我们通过引用对象的属性 key 来获取一个 value 时,它会触发*[[Get]]*的操作;

  • 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;

  • 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

  • 答案是有的,只要是对象都会有这样的一个内置属性;

获取对象原型的方式有两种:

  • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);

  • 方式二:通过 Object.getPrototypeOf() 方法可以获取到;

示例:

1、获取对象的原型

image-20230708174454487

2、对象属性查找顺序

image-20230708174517850

函数的原型

那么我们知道上面的东西对于我们的构造函数创建对象来说有什么作用呢?

  • 它的意义是非常重大的,接下来我们继续来探讨;

1、将函数看做一个普通的对象,它具备*__proto__*(隐式原型)属性

作用:查找 key 对应的 value 时,会找到原型身上

image-20230708174928716

2、将函数看做一个函数时,它具备*prototype*(显式原型)属性(注意:不是__proto__[[Prototype]]

作用:当通过 new 创建对象实例时,对象实例的隐式原型会指向这个构造函数的显式原型:foo.__proto__ = Foo.prototype

注意: 箭头函数没有原型prototype

image-20230620110250315

你可能会问题,老师是不是因为函数是一个对象,所以它有 prototype 的属性呢?

  • 不是的,因为它是一个函数,才有了这个特殊的属性;

  • 而不是它是一个对象,所以有这个特殊的属性;

image-20230620110300086

new、constructor

new 操作原型赋值

我们前面讲过 new 关键字的步骤如下:

  • 1、在内存中创建一个新的对象(空对象);

  • 2、这个对象内部的*[[prototype]]属性会被赋值为该构造函数的prototype 属性*;

那么也就意味着我们通过 Person 构造函数创建出来的所有对象的[[prototype]]属性都指向 Person.prototype:

image-20230620110309342

image-20230620110317715

constructor 属性

事实上原型对象上面是有一个属性的:constructor

  • 默认情况下原型上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象;

image-20230620110450469

image-20230717180007905

实例方法-构造函数和原型结合

我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如 running、eating 这些函数

  • 那么有没有办法让所有的对象去共享这些函数呢?

  • 可以,将这些函数放到 Person.prototype 的对象上即可;

image-20230620110549231

分析:

  • 1、p1 的隐式原型是 Person.prototype 对象
  • 2、p1.running 查找规则:
    • 先在自己身上查找,没有找到
    • 再去原型上查找,找到了

作用:

当多个对象拥有共同的值时,可以将该值放到构造函数的对象的显式原型上;由构造函数创建出来的所有对象,都会共享这些方法

内存图-创建实例对象

js
function Person(name, age) {
  this.name = name;
  this.age = age;
}

var p1 = new Person("mr", 18);
var p2 = new Person("tom", 20);

image-20230718101903460

内存图-添加原型属性

js
function Person(name, age) {
    this.name = name
    this.age = age
}

var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)

// 添加原型属性
+ Person.prototype.message = "中国"
+ p1.__proto__.info = "中国很美丽"

image-20230718102214052

内存图-添加原型方法

js
function Person(name, age) {
    this.name = name
    this.age = age
}

var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)

Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"

// 添加原型方法
+ Person.prototype.running = function() {}

image-20230718102903228

内存图-新增实例属性

js
function Person(name, age) {
    this.name = name
    this.age = age
}

var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)

Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"

// 修改p1.message,p2.message是否改变
+ p1.message = "美国"
+ console.log(p2.message) // => "中国"

image-20230718102548130

重写原型对象

在原有的原型对象上添加新的属性

image-20230718134758475

如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象

image-20230620110500575

前面我们说过, 每创建一个函数, 就会同时创建它的 prototype 对象, 这个对象也会自动获取 constructor 属性;

而我们这里相当于给 prototype 重新赋值了一个对象, 那么这个新对象的 constructor 属性, 会指向 Object 构造函数, 而不是

Person 构造函数

手动添加 constructor

如果希望 constructor 指向 Person,那么可以手动添加 constructor

上面的方式虽然可以, 但是也会造成 constructor 的[[Enumerable]]特性被设置了 true

  • 默认情况下, 原生的 constructor 属性是不可枚举的

  • 如果希望解决这个问题, 就可以使用我们前面介绍的 Object.defineProperty()函数了

1、手动添加 constructor,指向 Person

image-20230718135306523

2、通过 defineProperty 设置 constructor 属性为不可枚举

image-20230718140113426

构造函数的类方法

添加在构造函数本身的方法,叫类方法

类方法可以在没有实例对象的情况下,调用函数

image-20230719150627747

继承

继承

面向对象有三大特性:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;

  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);

  • 多态:不同的对象在执行时表现出不同的形态;

那么这里我们核心讲继承。

那么继承是做什么呢?

  • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;

  • 在很多编程语言中,继承是多态的前提

那么 JavaScript 当中如何实现继承呢?

  • 不着急,我们先来看一下 JavaScript 原型链的机制;

  • 再利用原型链的机制实现一下继承;

示例:

Student 类

image-20230718142304720

Teacher 类

image-20230718142308066

将共同的属性和方法抽取到父类中

原型链

JS 原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。

我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

js
const obj = {
  name: "mr",
  age: 18,
};
obj.__proto__ = {
  // message: 'hello aaa'
};
obj.__proto__ = {
  // message: 'hello bbb'
};
obj.__proto__.__proto__ = {
  message: "hello ccc",
};

原型链查找顺序图:

image-20230718151631191

const obj = {} 相当于 const obj = new Object() ,所以 obj 是有原型对象的,它的原型对象就是 Object 对象

Object 的原型

那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?

image-20230620110628158

我们会发现它打印的是 [Object: null prototype] {}

  • 事实上这个原型就是我们最顶层的原型

  • 从 Object 直接创建出来的对象的原型都是 [Object: null prototype] {}。

那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;

  • 特殊二:该对象上有很多默认的属性和方法

内存图-创建 Object 对象

image-20230718152955425

内存图-原型链关系

image-20230718153827321

Object 是所有类的父类

从我们上面的 Object 原型我们可以得出一个结论:原型链最顶层的原型对象就是 Object 的原型对象

image-20230719103230081

image-20230620110727856

实现继承-原型链-继承方法

通过原型链实现继承

如果我们现在需要实现继承,那么就可以利用原型链来实现了:

  • 目前 stu 的原型是 p 对象,而 p 对象的原型是 Person 默认的原型,里面包含 running 等函数;

  • 注意:步骤 3 和步骤 4 不可以调整顺序,否则会有问题

定义父类 Person

js
///1、定义父类构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {};
Person.prototype.eating = function () {};

实现继承: 创建一个父类的实例对象new Person(), 用这个实例对象作为子类的原型对象

js
///2、定义子类构造函数
function Student(sno) {
    this.sno = sno
}

// 3、创建父类实例,用它作为子类的原型对象
+ const p = new Person("mr", 18)
+ Student.prototype = p

// 4、为子类添加原型方法
Student.prototype.studying = function() {}

image-20230718163236591

~~错误的实现继承做法:~~父类的原型直接赋值给子类的原型

问题:父类和子类共享同一个原型对象,修改了任意一个,另外一个也被修改了

image-20230718155326664

image-20230718155305554

原型链继承的弊端

但是目前有一个很大的弊端:某些属性其实是保存在 p 对象上的;

  • 第一,我们通过直接打印对象是看不到这个属性的;

  • 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;

  • 第三,不能给 Person 传递参数(让每个 stu 有自己的属性),因为这个对象是一次性创建的(没办法定制化);

实现继承-构造函数-继承属性

借用构造函数继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):

  • steal 是偷窃、剽窃的意思,但是这里可以翻译成借用;

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数

  • 因为函数可以在任意的时刻被调用;
    • 因此通过 apply()和 call()方法也可以在新创建的对象上执行构造函数;

image-20230718165801884

image-20230718165804660

image-20230718172145997

组合借用继承的问题

组合继承是 JavaScript 最常用的继承模式之一:

  • 如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大;

  • 但是它依然不是很完美,但是基本已经没有问题了;

组合继承存在什么问题呢?

  • 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数

    • 一次在创建子类原型的时候;
    • 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
  • 另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性

    • 一份在当前的实例自己里面(也就是 person 本身的),另一份在子类对应的原型对象中(也就是 person.__proto__里面);
    • 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;

实现继承-寄生组合

原型式继承函数

原型式继承的渊源

  • 这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起:Prototypal Inheritance in JavaScript(在 JavaScript 中使用原型式继承)

  • 在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.

  • 为了理解这种方式,我们先再次回顾一下 JavaScript 想实现继承的目的:重复利用另外一个对象的属性和方法.

最终的目的:student 对象的原型指向了 person 对象;

image-20230620110938033

image-20230620110943607

image-20230620110955167

创建中间原型对象的方法:

方法一: 创建父类实例,用它作为子类的原型对象

image-20230718173344777

方法二: 创建空对象,该对象的隐式原型指向父类的原型对象,同时子类的原型对象指向该对象

image-20230718173442136

方法三:

image-20230718173733321

方法四:

image-20230718174035643

封装 1:

image-20230718174527911

image-20230718174330856

封装 2: 寄生组合式继承(最终方案):考虑兼容问题

image-20230718175009705

image-20230718174330856

寄生式继承函数

寄生式(Parasitic)继承

  • 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;

  • 寄生式继承的思路是结合原型类继承和工厂模式的一种方式;

  • 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;

image-20230620111016083

image-20230620111022272

寄生组合式继承

现在我们来回顾一下之前提出的比较理想的组合继承

  • 组合继承是比较理想的继承方式, 但是存在两个问题:

  • 问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.

  • 问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.

事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉

  • 你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.

  • 这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.

  • 能不能直接让子类型的原型对象 = 父类型的原型对象呢?

  • 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.

  • 我们使用前面的寄生式思想就可以了.

寄生组合继承的代码

image-20230620111044631

终极方案:寄生组合式继承▸

使用到的知识点:原型链、借用构造函数、原型式继承(对象之间)、寄生式函数

1、寄生组合式继承

image-20230718180010971

2、使用寄生组合式继承实现继承

image-20230718175958239

image-20230718180111696

3、打印结果

image-20230718180128078

image-20230718180130986

4、内存图

image-20230719103645923

实现继承-ES6

对象的方法补充

  • Object.prototype.hasOwnProperty(prop)返回:boolean,对象是否有某一个只属于自己属性(不是在原型上的属性)
    • 参数
    • prop:``,要测试的属性的字符串名称或者 Symbol
    • 返回值
    • 如果对象有指定属性作为自有属性,则返回 true;否则返回 false
  • in返回:,判断某个属性是否在某个对象自己或者对象的原型链
  • for...in返回:,遍历某个对象自己上或者其原型链上所有可枚举的属性(除 Symbol 外)
  • instanceof返回:,用于*检测构造函数*(Person、Student 类)的 pototype是否出现在某个实例对象的原型链上
  • Object.prototype.isPrototypeOf(obj)返回:boolean,用于*检测某个对象*是否出现在某个实例对象的原型链上
    • 参数
    • obj:``,要搜索其原型链的对象。

示例:

1、hasOwnProperty

image-20230719104927156

2、in 操作符

image-20230719105148980

3、for...in 操作符

image-20230719105618291

4、instanceof

image-20230719110029949

image-20230719110010661

5、isPrototypeOf

image-20230719111925815

原型继承关系

image-20230620111924721

内存图

image-20230719130453695

class 类-定义类

我们会发现,按照前面的构造函数形式创建 ,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。

  • ES6(ECMAScript2015)新的标准中使用了 class 关键字来直接定义类

  • 但是类本质上依然是前面所讲的构造函数、原型链语法糖而已;

  • 所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;

那么,如何使用 class 来定义一个类呢?

  • 可以使用两种方式来声明类:类声明类表达式

image-20230620111948847

class 类-构造函数

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

  • 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor

  • 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;

  • 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;

image-20230719153159904

当我们通过new关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作

  • 1、在内存中创建一个新的对象(空对象);

  • 2、这个对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性;

  • 3、构造函数内部的 this,会指向创建出来的新对象;

  • 4、执行构造函数的内部代码(函数体代码);

  • 5、如果构造函数没有返回非空对象,则返回创建出来的新对象;

class 类-实例方法

在上面我们定义的属性都是直接放到了 this 上,也就意味着它是放到了创建出来的新对象中:

  • 在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;

  • 这个时候我们可以直接在类中定义实例方法

  • 类中定义多个方法,不需要用,分割

image-20230719153654047

class 类-访问器方法▸

我们之前讲对象的属性描述符时有讲过对象可以添加settergetter函数的,那么类也是可以的

image-20230719160622182

class 类-静态方法▸

静态方法通常用于定义直接使用类来执行的方法不需要有类的实例,使用static 关键字来定义:

image-20230719162210655

class 类和构造函数的异同

我们来研究一下类的一些特性:

  • 你会发现它和我们的构造函数的特性其实是一致的;

1、构造函数定义的类

image-20230719154121322

2、class 定义的类

image-20230719154156559

3、相同点

image-20230620112001919

4、不同点

构造函数可以当做普通的函数调用,而 class 类不能

image-20230719154651673

image-20230719154632274

ES6 类的继承-extends

前面我们花了很大的篇幅讨论了在 ES5 中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。

  • 在 ES6 中新增了使用extends 关键字,可以方便的帮助我们实现继承

image-20230620112057839

ES6 类的继承-super

class 为我们的方法中提供了super关键字

  • 执行*super.method(...)*来调用一个父类方法
  • 执行*super(...)*来调用一个父类 constructor(只能在子类的 constructor 中执行 super)

image-20230620112108271

我们会发现在上面的代码中我使用了一个 super 关键字,这个 super 关键字有不同的使用方式:

注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过 super 调用父类的构造函数

super 的使用位置有三个:子类的构造方法实例方法静态方法

示例:

1、在子类的构造方法中使用 super

image-20230719163949393

2、在子类的实例方法中使用 super(方法重写

image-20230719164606937

3、在子类的静态方法中使用 super(方法重写

image-20230719164828968

继承内置类

我们也可以让我们的类继承自内置类,比如 Array:

image-20230620112117242

类的混入-mixin▸

JavaScript 的类只支持单继承:也就是只能有一个父类

  • 那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢?

  • 这个时候我们可以使用混入(mixin);

image-20230620111340568

image-20230620111354367

~~应用:~~React 中的高阶组件

image-20230719170846017

Babel

babel-ES6 转 ES5(源码)

1、简单类 Person 转 ES5

js
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  running() {}
  eating() {}
  static radomPerson() {}
}
const p1 = new Person("tom", 18);

image-20230719175341735

image-20230719175400459

2、继承类 Person 转 ES5

js
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  running() {}
  eating() {}
  static radomPerson() {}
}

class Student extends Person {
  constructor(name, age, sno, score) {
    super(name, age);
    this.sno = sno;
    this.score = score;
  }

  studyding() {}
  static radomStudent() {}
}

const stu1 = new Student("mr", 18, 110, 100);

ES5 代码

js
function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true },
  });
  Object.defineProperty(subClass, "prototype", { writable: false });
  if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf
    ? Object.setPrototypeOf.bind()
    : function _setPrototypeOf(o, p) {
        o.__proto__ = p;  // Student.__proto__ = Person
        return o;
      };
  return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  } else if (call !== void 0) {
    throw new TypeError(
      "Derived constructors may only return object or undefined"
    );
  }
  return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}
function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
    return true;
  } catch (e) {
    return false;
  }
}
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf.bind()
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}
function _typeof(obj) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj;
          }
        : function (obj) {
            return obj &&
              "function" == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? "symbol"
              : typeof obj;
          }),
    _typeof(obj)
  );
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, "string");
  return _typeof(key) === "symbol" ? key : String(key);
}
function _toPrimitive(input, hint) {
  if (_typeof(input) !== "object" || input === null) return input;
  var prim = input[Symbol.toPrimitive];
  if (prim !== undefined) {
    var res = prim.call(input, hint || "default");
    if (_typeof(res) !== "object") return res;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return (hint === "string" ? String : Number)(input);
}
var Person = /*#__PURE__*/ (function () {
  function Person(name, age) {
    _classCallCheck(this, Person);
    this.name = name;
    this.age = age;
  }
  _createClass(
    Person,
    [
      {
        key: "running",
        value: function running() {},
      },
      {
        key: "eating",
        value: function eating() {},
      },
    ],
    [
      {
        key: "radomPerson",
        value: function radomPerson() {},
      },
    ]
  );
  return Person;
})();

var Student = /*#__PURE__*/ (function (_Person) {
  _inherits(Student, _Person);
  var _super = _createSuper(Student);
  function Student(name, age, sno, score) {
    var _this;
    _classCallCheck(this, Student);
    _this = _super.call(this, name, age);
    _this.sno = sno;
    _this.score = score;
    return _this;
  }
  _createClass(
    Student,
    [
      {
        key: "studyding",
        value: function studyding() {},
      },
    ],
    [
      {
        key: "radomStudent",
        value: function radomStudent() {},
      },
    ]
  );
  return Student;
})(Person);
var stu1 = new Student("mr", 18, 110, 100);

多态

JavaScript 中的多态

面向对象的三大特性:封装、继承、多态。

  • 前面两个我们都已经详细解析过了,接下来我们讨论一下 JavaScript 的多态。

JavaScript 有多态吗?

  • 维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。

  • 非常的抽象,个人的总结:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。

那么从上面的定义来看,JavaScript是一定存在多态的。

1、JS中的多态

image-20230620112128310

image-20230725172438843

2、严格语言中的多态条件:

1、必须有继承或接口

2、必须有父类引用指向子类对象

image-20230725171526795

image-20230725171533229

对象字面量增强

image-20230725173108871

1、属性的简写

image-20230725173356513

2、方法的简写

image-20230725173807938

3、计算属性名

image-20230725174102227

解构

image-20230725174351476

1、数组的解构

  • 基本使用

image-20230725174637136

  • 顺序问题:有严格的顺序

image-20230725174856412

  • 解构出数组

image-20230725175013950

  • 解构的默认值

image-20230725175057806

image-20230725175127062

2、对象的解构

  • 基本使用

image-20230725174722608

  • 顺序问题:对象的解构没有顺序,是根据key来解构的

image-20230725175329604

  • 重命名变量

image-20230725175615243

  • 默认值

image-20230725175723447

  • 对象的剩余内容

image-20230725175832806

3、解构的应用

image-20230725180030731

手写

手写 call,aplly,bind

函数对象原型关系

函数 foo 对象的隐式原型 === Function 的显式原型

js
// 函数foo对象的隐式原型 === Function的显式原型
console.log(foo.__proto__ === Function.prototype); // true

console.log(Function.prototype.apply); // f apply()
console.log(Function.prototype.call); // f call()
console.log(Function.prototype.bind); // f bind()

console.log(Function.prototype.apply === foo.apply); // true

结论:

  1. foo对象中的某些属性和方法是来自 Function.prototype 的
  2. 在 Function.prototype 中添加的属性和方法,可以被所有的函数获取

image-20230224205957711

image-20230224210004633

在 Function 的原型中添加方法 bar

image-20230224211742738

手写 apply 方法

image-20230224211954891

给函数对象添加方法

image-20230224212210173

js
function foo() {
  console.log("foo", this);
}

Function.prototype.mrapply = function (mrthis) {
  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, "fn", {
    configurable: true,
    value: this,
  });
  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn();
  // 删除多出来的临时函数fn
  delete mrthis.fn;
};

foo.mrapply({ name: "Tom" });

如果传入的参数是一个 String 或者 Number 的类型,需要将其包裹成对象类型,才能在它上面添加属性

image-20230224214610343

image-20230224214547854

调用 mrapply 时,传递参数

image-20230224214829699

js
    function foo (age, height) {
      console.log('foo', this, age, height)
    }

+    Function.prototype.mrapply = function(mrthis, args) {
      // 当this不是对象时,需要用Object包裹
      mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)

      // 相当于 mrthis.fn = this
      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })

      // 隐式调用fn,可以让fn函数的this指向 mrthis
+      mrthis.fn(...args)

      // 删除多出来的临时函数fn
      delete mrthis.fn
    }

+    foo.mrapply({name: "Tom"}, [18, 1.88])
    foo.mrapply(null, [18, 1.88])
    foo.mrapply(undefined, [18, 1.88])
    foo.mrapply(true, [18, 1.88])
    foo.mrapply(123, [18, 1.88])
    foo.mrapply('aaaa', [18, 1.88])

手写 call 方法

js
    function foo(age, height) {
      console.log('foo', this, age, height)
    }

+    Function.prototype.mrcall = function(mrthis, ...args) {
      mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)

      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })

+      mrthis.fn(...args)

      delete mrthis.fn
    }

+    foo.mrcall({ name: "张飞" }, 20, 1.77)

抽取封装公共函数

js
    /* 抽取封装的函数 */
+    Function.prototype.mrexec = function(mrthis, args) {
      mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
      // mrthis.fn = this
      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })
      mrthis.fn(...args)
      delete mrthis.fn
    }

    /* 手写apply */
    Function.prototype.mrapply = function(mrthis, args) {
      this.mrexec(mrthis, args)
    }

    /* 手写call */
    Function.prototype.mrcall = function(mrthis, ...args) {
      this.mrexec(mrthis, args)
    }

    // 测试
    function foo(age, height) {
      console.log('foo', this, age, height)
    }
    foo.mrapply({name: "Tom"}, [19, 1.66])
    foo.mrcall({name: "Jack"}, 22, 1.99)

手写 bind 方法

和 apply, call 不同,bind 执行后是返回一个新的函数 newFoo

image-20230225114537180

基础实现

思路:想办法实现如下:

js
// 伪代码
{ name: "why" }.foo(name, age)
js
    /* 手写bind */
    Function.prototype.mrbind = function(mrthis, ...args) {
+      return (...moreArgs) => {
        mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
        Object.defineProperty(mrthis, 'fn', {
          configurable: true,
          value: this
        })
+        const allArgs = [...args, ...moreArgs]
+        mrthis.fn(...allArgs)
+        delete mrthis.fn // 可以删除fn,因为每次调用newFoo,都会重新生成一个mrthis.fn
      }
    }

    // 测试
    function foo(name, age, height, address) {
      console.log('foo', this, name, age, height, address)
    }
    const newFoo = foo.mrbind({name: "Jerry"}, '张飞', 45)
    console.log(newFoo)
+    newFoo(1.88, '成都')
+ 	 newFoo(1.88, '成都')

浅拷贝,深拷贝

引用赋值

image-20230228131039985

浅拷贝

方式:

  • 解构赋值:const info = {...obj}

image-20230228131318516

浅拷贝修改 info2.name 后,obj 的 name 依然是"why",被修改的只是 info2

image-20230228131406936

浅拷贝的内存图

image-20230228131822253

如果 obj 对象中有**其他对象(或数组)**时的内存图

image-20230228132402319

深拷贝

方式:

  • 1、借助第三方库:underscore
  • 2、利用现有 JS 机制:JSON
  • 3、自己实现:

2、利用现有 JS 机制:JSON

语法:

js
const info3 = JSON.parse(JSON.stringify(obj));

缺点: 该方法不能实现方法的深拷贝,会忽略 obj 对象中的方法

js
    const obj = {
      name: 'Tom',
      age: 18,
      friend: {
        name: 'Jack'
      },
      run: function() {
        console.log(this.name + '在跑步~');
      }
    }

    // 利用JSON机制实现深拷贝
+    const info = JSON.parse(JSON.stringify(obj))

    // 测试
    console.log(info)
 	// 修改info的深度属性,obj的深度属性保持不变
+    info.friend.name = '张飞'
+    console.log('obj', obj.friend.name); // obj Jack
+    console.log('info', info.friend.name); // obj 张飞

	// 不能实现方法的深拷贝,会忽略obj对象中的方法
+    info.run() // ncaught TypeError: info.run is not a function